﻿using System.Collections.Generic;

namespace MuHelper
{
    public struct NumberWithMeasure
    {
        /// <summary>
        /// Numeric value
        /// </summary>
        public readonly decimal Value;

        /// <summary>
        /// Measure
        /// </summary>
        public readonly Measure Measure;

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="value">numeric value</param>
        /// <param name="measure">measure</param>
        public NumberWithMeasure(decimal value, Measure measure)
        {
            this.Value = value;
            this.Measure = measure;
        }

        /// <summary>
        /// Converts current value to target measure if possible
        /// </summary>
        /// <param name="measure"></param>
        /// <returns></returns>
        public NumberWithMeasure ToMeasure(Measure measure)
        {
            if( !this.Measure.IsEquivalentTo(measure) )
            {
                return NaN;
            }

            decimal value = this.Value*this.Measure.Coefficient/measure.Coefficient;
            return new NumberWithMeasure(value, measure);
        }

        public override bool Equals(object obj)
        {
            if( ReferenceEquals(null, obj) ) return false;
            
            if (obj.GetType() != typeof (NumberWithMeasure) ) return false;

            return Equals((NumberWithMeasure) obj);
        }

        public bool Equals(NumberWithMeasure other)
        {
            return other.Value == Value && Equals(other.Measure, Measure);
        }

        public override int GetHashCode()
        {
            unchecked
            {
                return (Value.GetHashCode() * 397) ^ (Measure != null ? Measure.GetHashCode() : 0);
            }
        }

        /// <summary>
        /// Value used to indicate that result of operation is not defined
        /// </summary>
        public static NumberWithMeasure NaN;
        
        /// <summary>
        /// static constructor
        /// </summary>
        static NumberWithMeasure()
        {
            NaN = new NumberWithMeasure(decimal.MaxValue, Measure.None);
        }

        public static bool operator ==(NumberWithMeasure value1, NumberWithMeasure value2)
        {
            return value1.Value == value2.Value && value1.Measure == value2.Measure;
        }

        public static bool operator !=(NumberWithMeasure value1, NumberWithMeasure value2)
        {
            return !(value1 == value2);
        }

        /// <summary>
        /// Sum operator
        /// </summary>
        /// <param name="value1">first argument</param>
        /// <param name="value2">second argument</param>
        /// <returns>sum</returns>
        public static NumberWithMeasure operator +(NumberWithMeasure value1, NumberWithMeasure value2)
        {
            // check if value1 can be converted to value2
            if( !value2.Measure.IsEquivalentTo(value1.Measure) )
            {
                return NaN;
            }

            // calculate result
            decimal value = value1.Value + value2.Value*value1.Measure.Coefficient/value2.Measure.Coefficient;
            return new NumberWithMeasure(value, value1.Measure);
        }

        /// <summary>
        /// Minus operator
        /// </summary>
        /// <param name="value1"></param>
        /// <param name="value2"></param>
        /// <returns></returns>
        public static NumberWithMeasure operator -(NumberWithMeasure value1, NumberWithMeasure value2)
        {
            // check if value1 can be converted to value2
            if (!value2.Measure.IsEquivalentTo(value1.Measure))
            {
                return NaN;
            }

            // calculate result
            decimal value = value1.Value + value2.Value * value1.Measure.Coefficient / value2.Measure.Coefficient;
            return new NumberWithMeasure(value, value1.Measure);
        }

        /// <summary>
        /// Multiply operator
        /// </summary>
        /// <param name="value1"></param>
        /// <param name="value2"></param>
        /// <returns></returns>
        public static NumberWithMeasure operator *(NumberWithMeasure value1, NumberWithMeasure value2)
        {
            Dictionary<string, decimal> baseMeasures;

            // both measure are base
            if( value1.Measure.Base && value2.Measure.Base )
            {
                baseMeasures = new Dictionary<string, decimal>(2)
                                    {
                                        {value1.Measure.Name, 1}, 
                                        {value2.Measure.Name, 1}
                                    };
            }
            else if( !value1.Measure.Base && !value2.Measure.Base ) // both measure are complex
            {
                int count = value1.Measure.Nodes.Count + value2.Measure.Nodes.Count;
                baseMeasures = new Dictionary<string, decimal>(count);

                foreach (var node in value1.Measure.Nodes)
                {
                    if (value2.Measure.Nodes.ContainsKey(node.Key))
                    {
                        var power = value1.Measure.Nodes[node.Key] + value2.Measure.Nodes[node.Key];
                        if (power == 0)
                            continue;

                        baseMeasures.Add(node.Key, power);
                    }
                    else
                    {
                        baseMeasures.Add(node.Key, node.Value);
                    }
                }

                foreach(var node in value2.Measure.Nodes)
                {
                    if( value1.Measure.Nodes.ContainsKey(node.Key) )
                        continue;

                    baseMeasures.Add(node.Key, node.Value);
                }
            }
            else if( value1.Measure.Base ) // one of measure are base, other - complex
            {
                baseMeasures = new Dictionary<string, decimal>(value2.Measure.Nodes.Count + 1);
                foreach (var node in value2.Measure.Nodes)
                {
                    baseMeasures.Add(node.Key, node.Value);
                }
                if( baseMeasures.ContainsKey(value1.Measure.Name) )
                {
                    baseMeasures[value1.Measure.Name] = baseMeasures[value1.Measure.Name] + 1;
                    if( baseMeasures[value1.Measure.Name] == 0 )
                        baseMeasures.Remove(value1.Measure.Name);
                }
            }
            else
            {
                baseMeasures = new Dictionary<string, decimal>(value1.Measure.Nodes.Count + 1);
                if( baseMeasures.ContainsKey(value1.Measure.Name) )
                {
                    baseMeasures[value2.Measure.Name] = baseMeasures[value2.Measure.Name] + 1;
                    if( baseMeasures[value2.Measure.Name] == 0 )
                        baseMeasures.Remove(value2.Measure.Name);
                }
            }

            decimal coefficient = value1.Measure.Coefficient * value2.Measure.Coefficient;
            decimal value = value1.Value * value2.Value;

            if (baseMeasures.ContainsKey("None"))
            {
                baseMeasures.Remove("None");
            }
            Measure measure = baseMeasures.Count == 0 
                                  ? Measure.None 
                                  : new Measure(coefficient, baseMeasures);
            return new NumberWithMeasure(value, measure);
        }

        /// <summary>
        /// Division operator
        /// </summary>
        /// <param name="value1"></param>
        /// <param name="value2"></param>
        /// <returns></returns>
        public static NumberWithMeasure operator /(NumberWithMeasure value1, NumberWithMeasure value2)
        {
            Dictionary<string, decimal> baseMeasures;

            // both measure are base
            if( value1.Measure.Base && value2.Measure.Base )
            {
                baseMeasures = new Dictionary<string, decimal>(2)
                                    {
                                        {value1.Measure.Name, 1}, 
                                        {value2.Measure.Name, -1}
                                    };
            }
            else if( !value1.Measure.Base && !value2.Measure.Base ) // both measure are complex
            {
                int count = value1.Measure.Nodes.Count + value2.Measure.Nodes.Count;
                baseMeasures = new Dictionary<string, decimal>(count);

                foreach( var node in value1.Measure.Nodes )
                {
                    if( value2.Measure.Nodes.ContainsKey(node.Key) )
                    {
                        var power = value1.Measure.Nodes[node.Key] - value2.Measure.Nodes[node.Key];
                        if( power == 0 )
                            continue;

                        baseMeasures.Add(node.Key, power);
                    }
                    else
                    {
                        baseMeasures.Add(node.Key, node.Value);
                    }
                }

                foreach( var node in value2.Measure.Nodes )
                {
                    if( value1.Measure.Nodes.ContainsKey(node.Key) )
                        continue;

                    baseMeasures.Add(node.Key, -node.Value);
                }
            }
            else if( value1.Measure.Base ) // one of measure are base, other - complex
            {
                baseMeasures = new Dictionary<string, decimal>(value2.Measure.Nodes.Count + 1);
                foreach( var node in value2.Measure.Nodes )
                {
                    baseMeasures.Add(node.Key, node.Value);
                }
                if( baseMeasures.ContainsKey(value1.Measure.Name) )
                {
                    baseMeasures[value1.Measure.Name] = baseMeasures[value1.Measure.Name] - 1;
                    if( baseMeasures[value1.Measure.Name] == 0 )
                        baseMeasures.Remove(value1.Measure.Name);
                }
            }
            else
            {
                baseMeasures = new Dictionary<string, decimal>(value1.Measure.Nodes.Count + 1);
                if( baseMeasures.ContainsKey(value1.Measure.Name) )
                {
                    baseMeasures[value2.Measure.Name] = baseMeasures[value2.Measure.Name] - 1;
                    if( baseMeasures[value2.Measure.Name] == 0 )
                        baseMeasures.Remove(value2.Measure.Name);
                }
            }

            decimal coefficient = value1.Measure.Coefficient / value2.Measure.Coefficient;
            decimal value = value1.Value / value2.Value;

            if( baseMeasures.ContainsKey("None") )
            {
                baseMeasures.Remove("None");
            }
            Measure measure = baseMeasures.Count == 0
                                  ? Measure.None
                                  : new Measure(coefficient, baseMeasures);
            return new NumberWithMeasure(value, measure);
        }

        /// <summary>
        /// Sing invertion operator
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static NumberWithMeasure operator -(NumberWithMeasure value)
        {
            if( value == NaN )
            {
                return NaN;
            }
            return new NumberWithMeasure(-value.Value, value.Measure);
        }

        public static NumberWithMeasure operator +(decimal value1, NumberWithMeasure value2)
        {
            return new NumberWithMeasure(value1, Measure.None) + value2;
        }

        public static NumberWithMeasure operator +(NumberWithMeasure value1, decimal value2)
        {
            return value1 + new NumberWithMeasure(value2, Measure.None);
        }

        public static NumberWithMeasure operator -(decimal value1, NumberWithMeasure value2)
        {
            return new NumberWithMeasure(value1, Measure.None) - value2;
        }

        public static NumberWithMeasure operator -(NumberWithMeasure value1, decimal value2)
        {
            return value1 - new NumberWithMeasure(value2, Measure.None);
        }

        public static NumberWithMeasure operator *(decimal value1, NumberWithMeasure value2)
        {
            return new NumberWithMeasure(value1, Measure.None) * value2;
        }

        public static NumberWithMeasure operator *(NumberWithMeasure value1, decimal value2)
        {
            return value1*new NumberWithMeasure(value2, Measure.None);
        }

        public static NumberWithMeasure operator /(decimal value1, NumberWithMeasure value2)
        {
            return new NumberWithMeasure(value1, Measure.None) / value2;
        }

        public static NumberWithMeasure operator /(NumberWithMeasure value1, decimal value2)
        {
            return value1 / new NumberWithMeasure(value2, Measure.None);
        }

        /// <summary>
        /// Convertion decimal to NumberWithMeasure without measure unit
        /// </summary>
        /// <param name="value">numeric value</param>
        /// <returns>return new number with measure with <see cref="NumberWithMeasure.Value"/> equal to <paramref name="value"/> and <see cref="Measure"/> equal to Measure.None</returns>
        public static implicit operator NumberWithMeasure(decimal value)
        {
            return new NumberWithMeasure(value, Measure.None);
        }

    }
}